/*
 * Copyright (C) 2007 OpenedHand
 * Copyright (C) 2007 Alp Toker <alp@atoker.com>
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "videosink.h"

#include <gst/gst.h>
#include <gst/video/video.h>

#include <glib.h>
#include <string.h>

static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
		GST_PAD_SINK,
		GST_PAD_ALWAYS,
		GST_STATIC_CAPS (GST_VIDEO_CAPS_RGBx ";" GST_VIDEO_CAPS_BGRx));

GST_DEBUG_CATEGORY_STATIC(galaxium_video_sink_debug);
#define GST_CAT_DEFAULT galaxium_video_sink_debug

static GstElementDetails galaxium_video_sink_details =
    GST_ELEMENT_DETAILS("Galaxium video sink",
                        "Sink/Video",
                        "Sends video data from a GStreamer pipeline to a Cairo surface",
                        "Paul Burton <paulburton89@gmail.com>");

enum
{
	PROP_0,
	PROP_SURFACE
};

struct _GalaxiumVideoSinkPrivate
{
	cairo_surface_t	*surface;
	GAsyncQueue		*async_queue;
	gboolean		rgb_ordering;
	int				width;
	int				height;
	int				fps_n, fps_d;
	int				par_n, par_d;
	NewSizeMethod	new_size_method;
	void			*new_size_data;
	NewFrameMethod	new_frame_method;
	void			*new_frame_data;
};

#define _do_init(bla) \
    GST_DEBUG_CATEGORY_INIT (galaxium_video_sink_debug, \
                             "galaxiumsink", \
                             0, \
                             "galaxium video sink")

GST_BOILERPLATE_FULL(GalaxiumVideoSink,
                     galaxium_video_sink,
                     GstBaseSink,
                     GST_TYPE_BASE_SINK,
                     _do_init);

static void galaxium_video_sink_base_init (gpointer g_class)
{
	GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
	
	gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&sinktemplate));
	gst_element_class_set_details (element_class, &galaxium_video_sink_details);
}

static void galaxium_video_sink_init (GalaxiumVideoSink *sink, GalaxiumVideoSinkClass *klass)
{
	GalaxiumVideoSinkPrivate *priv;
	
	sink->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (sink, GALAXIUM_TYPE_VIDEO_SINK, GalaxiumVideoSinkPrivate);
	priv->async_queue = g_async_queue_new();
}

static gboolean galaxium_video_sink_idle_func (gpointer data)
{
	GalaxiumVideoSinkPrivate* priv = (GalaxiumVideoSinkPrivate *)data;
	GstBuffer* buffer;
	
	if (!priv->async_queue)
		return FALSE;
	
	buffer = (GstBuffer *)g_async_queue_try_pop (priv->async_queue);
	
	if (buffer == NULL || G_UNLIKELY (!GST_IS_BUFFER (buffer)))
		return FALSE;
	
	// TODO: consider priv->rgb_ordering?
	cairo_surface_t* src = cairo_image_surface_create_for_data (GST_BUFFER_DATA (buffer), CAIRO_FORMAT_RGB24, priv->width, priv->height, (4 * priv->width + 3) & ~ 3);
	
	// TODO: We copy the data twice right now. This could be easily improved.
	cairo_t* cr = cairo_create (priv->surface);
	cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
	cairo_set_source_surface (cr, src, 0, 0);
	cairo_surface_destroy (src);
	cairo_rectangle (cr, 0, 0, priv->width, priv->height);
	cairo_fill (cr);
	cairo_destroy (cr);
	
	gst_buffer_unref (buffer);
	
	if (priv->new_frame_method != NULL)
		priv->new_frame_method (priv->new_frame_data);
	
	return FALSE;
}

static GstFlowReturn galaxium_video_sink_render (GstBaseSink *bsink, GstBuffer *buffer)
{
	GalaxiumVideoSink *sink = GALAXIUM_VIDEO_SINK(bsink);
	GalaxiumVideoSinkPrivate *priv = sink->priv;
	
	g_async_queue_push(priv->async_queue, gst_buffer_ref(buffer));
	g_idle_add_full(G_PRIORITY_HIGH_IDLE, galaxium_video_sink_idle_func, priv, NULL);
	
	return GST_FLOW_OK;
}

static gboolean galaxium_video_sink_set_caps (GstBaseSink *bsink, GstCaps *caps)
{
	GalaxiumVideoSink *sink = GALAXIUM_VIDEO_SINK (bsink);
	GalaxiumVideoSinkPrivate *priv = sink->priv;
	GstStructure *structure;
	gboolean ret;
	const GValue *fps;
	const GValue *par;
	gint width, height;
	int red_mask;
	
	GstCaps *intersection = gst_caps_intersect (gst_static_pad_template_get_caps (&sinktemplate), caps);
	
	if (gst_caps_is_empty (intersection))
		return FALSE;
	
	gst_caps_unref (intersection);
	
	structure = gst_caps_get_structure (caps, 0);
	
	ret = gst_structure_get_int (structure, "width", &width);
	ret &= gst_structure_get_int (structure, "height", &height);
	fps = gst_structure_get_value (structure, "framerate");
	ret &= (fps != NULL);
	
	par = gst_structure_get_value (structure, "pixel-aspect-ratio");
	
	if (!ret)
		return FALSE;
	
	if ((priv->width != width) || (priv->height != height))
	{
		if (priv->new_size_method != NULL)
			priv->new_size_method (priv->new_size_data, width, height);
	}
	
	priv->width = width;
	priv->height = height;
	
	/* We dont yet use fps or pixel aspect into but handy to have */
	priv->fps_n = gst_value_get_fraction_numerator (fps);
	priv->fps_d = gst_value_get_fraction_denominator (fps);
	
	if (par)
	{
		priv->par_n = gst_value_get_fraction_numerator (par);
		priv->par_d = gst_value_get_fraction_denominator (par);
	}
	else
		priv->par_n = priv->par_d = 1;
	
	gst_structure_get_int (structure, "red_mask", &red_mask);
	priv->rgb_ordering = (red_mask == 0xff000000);
	
	return TRUE;
}

static void galaxium_video_sink_dispose(GObject* object)
{
    GalaxiumVideoSink* sink = GALAXIUM_VIDEO_SINK(object);
    GalaxiumVideoSinkPrivate* priv = sink->priv;

    if (priv->surface) {
        cairo_surface_destroy(priv->surface);
        priv->surface = NULL;
    }

    if (priv->async_queue) {
        g_async_queue_unref(priv->async_queue);
        priv->async_queue = NULL;
    }

    G_OBJECT_CLASS(parent_class)->dispose(object);
}

static void galaxium_video_sink_finalize (GObject *object)
{
	//GalaxiumVideoSink *sink = GALAXIUM_VIDEO_SINK (object);
	
	G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
galaxium_video_sink_set_property(GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec)
{
    GalaxiumVideoSink* sink = GALAXIUM_VIDEO_SINK(object);
    GalaxiumVideoSinkPrivate* priv = sink->priv;

    switch (prop_id) {
    case PROP_SURFACE:
        if (priv->surface)
            cairo_surface_destroy(priv->surface);
        priv->surface = cairo_surface_reference((cairo_surface_t*)g_value_get_pointer(value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
galaxium_video_sink_get_property(GObject* object, guint prop_id, GValue* value, GParamSpec* pspec)
{
    GalaxiumVideoSink* sink = GALAXIUM_VIDEO_SINK(object);

    switch (prop_id) {
    case PROP_SURFACE:
        g_value_set_pointer(value, sink->priv->surface);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static gboolean galaxium_video_sink_stop (GstBaseSink *base_sink)
{
	GalaxiumVideoSinkPrivate *priv = GALAXIUM_VIDEO_SINK (base_sink)->priv;
	GstBuffer *buffer;
	
	g_async_queue_lock (priv->async_queue);
	
	/* Remove all remaining objects from the queue */
	do
	{
		buffer = g_async_queue_try_pop_unlocked (priv->async_queue);
		
		if (buffer)
			gst_buffer_unref (buffer);
	} while (buffer != NULL);
	
	g_async_queue_unlock (priv->async_queue);
	
	return TRUE;
}

static void galaxium_video_sink_class_init (GalaxiumVideoSinkClass* klass)
{
	GObjectClass* gobject_class = G_OBJECT_CLASS (klass);
	GstBaseSinkClass* gstbase_sink_class = GST_BASE_SINK_CLASS (klass);
	
	g_type_class_add_private (klass, sizeof (GalaxiumVideoSinkPrivate));
	
	gobject_class->set_property = galaxium_video_sink_set_property;
	gobject_class->get_property = galaxium_video_sink_get_property;
	
	gobject_class->dispose = galaxium_video_sink_dispose;
	gobject_class->finalize = galaxium_video_sink_finalize;
	
	gstbase_sink_class->render = galaxium_video_sink_render;
	gstbase_sink_class->preroll = galaxium_video_sink_render;
	gstbase_sink_class->stop = galaxium_video_sink_stop;
	gstbase_sink_class->set_caps = galaxium_video_sink_set_caps;
	
	g_object_class_install_property (gobject_class, PROP_SURFACE,
		g_param_spec_pointer ("surface", "surface", "Target cairo_surface_t*", (GParamFlags)(G_PARAM_READWRITE)));
}

/**
 * galaxium_video_sink_new:
 * @surface: a #cairo_surface_t
 *
 * Creates a new GStreamer video sink which uses @surface as the target
 * for sinking a video stream from GStreamer.
 *
 * Return value: a #GstElement for the newly created video sink
 */
GstElement* galaxium_video_sink_new (NewSizeMethod new_size, void *new_size_data,
									 NewFrameMethod new_frame, void *new_frame_data)
{
	GalaxiumVideoSink *sink = (GalaxiumVideoSink *)g_object_new (GALAXIUM_TYPE_VIDEO_SINK, NULL);
	GalaxiumVideoSinkPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (sink, GALAXIUM_TYPE_VIDEO_SINK, GalaxiumVideoSinkPrivate);
	
	priv->new_size_method = new_size;
	priv->new_size_data = new_size_data;
	priv->new_frame_method = new_frame;
	priv->new_frame_data = new_frame_data;
	
	return (GstElement *)sink;
}

void galaxium_video_sink_set_surface (GalaxiumVideoSink *sink, cairo_surface_t *surface)
{
	GalaxiumVideoSinkPrivate *priv;
	sink->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (sink, GALAXIUM_TYPE_VIDEO_SINK, GalaxiumVideoSinkPrivate);
	
	if (priv->surface)
		cairo_surface_destroy (priv->surface);
	
	priv->surface = cairo_surface_reference (surface);
}
